// 版权归Go作者所有。版权所有。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

// 清除免费页面。
// 
// 此文件实现清除堆中空闲和未使用的页面（释放支持映射
// 内存的物理页面），以处理页面级
// 碎片并减少Go应用程序的RSS。
// 
// Go中的清除发生在两个方面：背景
// （异步）清除程序和堆增长（同步）清除程序。
// 
// 前者发生在goroutine上，非常类似于背景清扫器
// 根据
// 清除成本的数量级估计，软限制在使用突变子时间的清除百分比。后台
// 清道夫的主要目标是将
// 应用程序的估计堆RSS降低到一个目标。
// 
// 该目标定义为：
// （retainExtraPercent+100）/100*（heapGoal/lastHeapGoal）*last_heap\u使用
// 
// 本质上，我们希望应用程序的RSS跟踪堆目标，但
// 堆目标是根据对象的字节定义的，而不是像
// RSS这样的页面。因此，我们需要考虑
// /跨度内部的碎片。heapGoal/lastHeapGoal定义当前堆目标
// 和上一个堆目标之间的比率，它告诉我们堆的增长量和收缩量。我们通过采用
// 这个比率并乘以最后一次GC结束时的heap_inuse来估计堆在页面方面的增长，
// 允许我们考虑这个额外的碎片。注意，这个过程假设碎片的程度在下一个GC循环中不会发生显著变化。高估
// 碎片的数量只会导致更高的内存使用率，这将在下一次调整更新时计入
// 中。但是，低估碎片
// 可能会导致性能下降。本案处理不在清道夫
// 范围内。在单个GC循环过程中碎片气球数量
// 的情况应视为病理，
// 标记为错误，并适当修复。
// 
// 添加了一个额外的retainExtraPercent系数作为缓冲区，以帮助确保
// 有更多未经修补的内存可供分配，因为每次分配
// 清除的内存都会导致潜在的昂贵页面错误。
// 
// 目标在每次GC后更新，清道夫的起搏参数
// （位于mheap____）更新为匹配。起搏参数的工作原理与背景扫描参数类似。这些参数定义了一条线，其
// 横轴是时间，纵轴是估计的堆RSS，并且
// 清道夫试图始终保持在该线以下。
// 
// 对于堆增长的某些定义，每当堆以
// 大小增长时，就会发生同步堆增长清除。这背后的直觉是，应用程序必须增加堆，因为现有的片段不够大，无法满足页面级内存分配，所以我们急切地清除这些片段，以抵消RSS的增长。

package runtime

import (
	"runtime/internal/atomic"
	"runtime/internal/sys"
	"unsafe"
)

const (
	// 背景清道夫根据这些参数进行调整。
	// 
	// 清除百分比代表我们愿意花费在清除上的变异时间的百分比。
	scavengePercent = 1 // 1%

	// retainExtraPercent表示堆目标
	// 上的内存量，清道夫应将其保留为分配器的缓冲空间。
	// 
	// 维护此开销的目的是为了拥有更大的
	// 非屏蔽内存池以供分配（因为使用清除内存
	// 会产生额外成本），要解释堆碎片和
	// 堆的不断变化的布局。
	retainExtraPercent = 10

	// maxPagesPerPhysPage是基于maxPhysPageSize的每个
	// 物理页面支持的最大运行时页面数。
	maxPagesPerPhysPage = maxPhysPageSize / pageSize

	// 清道夫是使用以前的
	// 清除内存和清除内存的成本之间的近似比率。
	// 
	// 对于大多数系统，清除的成本远远超过与使用清除内存相关的成本，使该常数为0。在其他系统
	// （尤其是那些“sysUsed”不仅仅是一个no op的系统）上，这个成本是不小的。
	// 
	// 此比率用作乘法因子的一部分，以帮助清道夫计算
	// 在其速度调整中使用清零内存的额外成本。
	scavengeCostRatio = 0.7 * (sys.GoosDarwin + sys.GoosIos)

	// 清道夫保留碎片决定清道夫
	// 一次应保留用于清道夫的内存量。具体地说，
	// 保留的内存量为（以字节为单位的堆大小）/CleavereServationShards。
	scavengeReservationShards = 64
)

// heapRetained返回当前堆RSS的估计值。
func heapRetained() uint64 {
	return memstats.heap_sys.load() - atomic.Load64(&memstats.heap_released)
}

// gcpacesvenger更新清道夫的配速，特别是
// 其速率和RSS目标。
// 
// RSS目标基于当前堆目标，开销较小
// 以适应分配器中的不确定性。
// 
// 起搏是基于CleavePagerate的，适用于常规页面和
// 大页面。有关更多信息，请参阅该常数。
// 
// mheap_u2;。必须持有锁，否则世界必须停止。
func gcPaceScavenger() {
	// 如果在第一次GC完成之前调用我们，请禁用清除。
	// 无论如何，我们在第二个GC循环之前都不会进行清理（我们还没有足够的关于堆的
	// 信息），所以这很好，并避免了错误
	// 或以后的垃圾数据。
	if gcController.lastHeapGoal == 0 {
		mheap_.scavengeGoal = ^uint64(0)
		return
	}
	// 计算我们的清除目标。
	goalRatio := float64(atomic.Load64(&gcController.heapGoal)) / float64(gcController.lastHeapGoal)
	retainedGoal := uint64(float64(memstats.last_heap_inuse) * goalRatio)
	// 将RetainExtracent开销添加到retainedGoal。这个计算看起来很奇怪，但目的是得到一个整数除法
	// （例如，如果retainExtraPercent=12.5，那么我们得到的除数是8）
	// ，这也避免了乘法的溢出。
	retainedGoal += retainedGoal / (1.0 / (retainExtraPercent / 100.0))
	// 将其与物理页面边界对齐，以使以下计算更加精确。
	retainedGoal = (retainedGoal + uint64(physPageSize) - 1) &^ (uint64(physPageSize) - 1)

	// 表示我们现在在堆中对RSS的贡献（字节）。
	// 
	// 在
	// PHYSPACESIZE<=pageSize的系统上保证始终是PHYSPACESIZE的倍数，因为我们以大于
	// 的速率映射heap_sys，并以PHYSPACESIZE的倍数释放内存。
	// 
	// 但是，某些函数将heap_sys重新分类为其他统计数据（例如，
	// stack_sys），这是以页面大小的倍数发生的，因此在系统
	// physpacgesize>pageSize中，下面的计算将不准确。
	// 一般来说，这是可以的，因为我们最多只会关闭一个常规
	// 物理页面。
	retainedNow := heapRetained()

	// 如果我们已经低于目标，或者距离目标不到一页，请禁用背景清道夫。如果
	// 要做的工作少于一个物理页面，我们将禁用后台清道夫，因为这不值得。
	if retainedNow <= retainedGoal || retainedNow-retainedGoal < uint64(physPageSize) {
		mheap_.scavengeGoal = ^uint64(0)
		return
	}
	mheap_.scavengeGoal = retainedGoal
}

// 后台清道夫的睡眠/等待状态。
var scavenge struct {
	lock       mutex
	g          *g
	parked     bool
	timer      *timer
	sysmonWake uint32 // 以原子方式设置。
}

// ReadyForCavenger向sysmon发送信号，让其唤醒清道夫，因为
// 可能有新的工作要做。
// 
// 此函数运行
// 与清除程序被踢醒之间可能存在明显延迟，但在直接调用WakeCleaver不安全的上下文中，可以安全地调用
// 此函数。
func readyForScavenger() {
	atomic.Store(&scavenge.sysmonWake, 1)
}

// 如有必要，唤醒清洗器立即断开清洗器的连接。
// 
// 可以在没有P的情况下运行，但它可以分配，因此在任何分配路径上都不能称为
// 。
// 
// mheap_uu2;.lock、扫掠.lock和sched.lock不能被保留。
func wakeScavenger() {
	lock(&scavenge.lock)
	if scavenge.parked {
		// 通知sysmon它不应该麻烦唤醒清道夫。
		atomic.Store(&scavenge.sysmonWake, 0)

		// 尝试停止计时，但我们并不在乎是否成功。
		// 可能是计时器从未启动，或者是
		// 我们正在与它赛跑。
		// 在我们与之比赛的情况下，
		// 我们经历了清道夫的虚假唤醒，但这是
		// 完全安全的。
		stopTimer(scavenge.timer)

		// 解开goroutine并告诉它可能有节奏
		// 变化。请注意，我们跳过了调度程序的runnext插槽，因为我们使用
		// 希望避免清道夫干扰公平
		// 用户goroutine的调度。实际上，这将
		// 清道夫安排在一个“较低优先级”上，但这没关系，因为当它被安排时，它将赶上它错过的工作。
		scavenge.parked = false

		// 通过注射来准备goroutine。我们使用injectglist代替ready或goready的
		// 以允许我们在没有P的情况下运行此函数
		// injectglist还避免将goroutine放置在
		// 当前P的runnext插槽中，这是为了防止清除程序对用户goroutine调度的干扰过多。
		var list gList
		list.push(scavenge.g)
		injectglist(&list)
	}
	unlock(&scavenge.lock)
}

// screevesleep尝试将清道夫休眠一段时间。
// 
// 请注意，此函数只能由清道夫调用。
// 
// 清道夫可能会因起搏改变而提前醒来，如果起搏改变待定，清道夫可能根本无法入睡。返回实际睡眠的时间量。
func scavengeSleep(ns int64) int64 {
	lock(&scavenge.lock)

	// 设置计时器。
	// 
	// 这必须发生在这里，而不是gopark内
	// 因为如果
	// 逃逸分析失败，我们无法关闭任何变量。
	start := nanotime()
	resetTimer(scavenge.timer, start+ns)

	// 把自己标记为睡着了，然后去睡觉。
	scavenge.parked = true
	goparkunlock(&scavenge.lock, waitReasonSleep, traceEvGoSleep, 2)

	// 返回我们实际睡了多长时间。
	return nanotime() - start
}

// 背景清道夫。
// 
// 后台清理程序在
// 以下维护应用程序的RSS，该行由
// mheap结构中的比例清理统计信息描述。
func bgscavenge() {
	scavenge.g = getg()

	lockInit(&scavenge.lock, lockRankScavenge)
	lock(&scavenge.lock)
	scavenge.parked = true

	scavenge.timer = new(timer)
	scavenge.timer.f = func(_ interface{}, _ uintptr) {
		wakeScavenger()
	}

	gcenable_setup <- 1
	goparkunlock(&scavenge.lock, waitReasonGCScavengeWait, traceEvGoBlock, 1)

	// 此
	// goroutine花费的清除时间分数的指数加权移动平均值（即单个CPU的百分比）。
	// 它表示调度开销的一种度量，可能会将睡眠或关键时间延长到超出预期的程度。假设一开始没有
	// 开销。ABCFDG 
	// 现在可行，因为清道夫通过
	// 清道夫操作获得堆锁，这意味着清道夫有效地阻止了
	// 分配器，并且不可扩展。但是，给定一个可伸缩的分配器，
	// 让清道夫也可以用它来伸缩；如果您的
	// 分配频率更高，那么您大概也在为清道夫生成
	// 更多的工作。
	const idealFraction = scavengePercent / 100.0
	scavengeEWMA := float64(idealFraction)

	for {
		released := uintptr(0)

		// 扫气临界段时间。
		crit := float64(0)

		// 在系统堆栈上运行，因为我们抓取了堆锁，
		// 并且堆栈随着堆锁增长意味着死锁。
		systemstack(func() {
			lock(&mheap_.lock)

			// 如果背景清除被禁用，或者如果没有工作要做，就停下来。
			retained, goal := heapRetained(), mheap_.scavengeGoal
			if retained <= goal {
				unlock(&mheap_.lock)
				return
			}

			// 清除一页，并测量清除所花费的时间。
			start := nanotime()
			released = mheap_.pages.scavenge(physPageSize, true)
			mheap_.pages.scav.released += released
			crit = float64(nanotime() - start)

			unlock(&mheap_.lock)
		})

		if released == 0 {
			lock(&scavenge.lock)
			scavenge.parked = true
			goparkunlock(&scavenge.lock, waitReasonGCScavengeWait, traceEvGoBlock, 1)
			continue
		}

		if released < physPageSize {
			// 如果发生这种情况，这意味着我们可能试图发布物理页面的部分
			// 但其可能的影响是它发布了整个物理页面，其中一些可能仍在使用。
			// 这可能导致内存损坏。投
			throw("released less than one physical page of memory")
		}

		// 在某些平台上，如果清除
		// 内存的时间小于其时钟的最小粒度（例如Windows），我们可能会将crit视为零。
		// 在这种情况下，只需假设清除每个常规物理页面需要10µs 
		// （根据经验确定），保守地忽略大页面
		// 对定时的影响。
		// 
		// 我们不应该看到低于零的暴击值，除非存在
		// 某种缺陷，无论是在我们这边还是在我们运行的平台上，但在这种情况下，也要采取防御措施。
		const approxCritNSPerPhysicalPage = 10e3
		if crit <= 0 {
			crit = approxCritNSPerPhysicalPage * float64(released/physPageSize)
		}

		// 将关键时间乘以1+使用
		// 清除内存与清除内存的成本比率。这迫使我们通过长时间的睡眠和更少的清理来支付急切地重用内存的成本。更具体地说，我们避免了这样的情况：由于使用被清除的内存会产生额外的开销，我们经常会进行清除，从而影响分配性能。
		crit *= 1 + scavengeCostRatio

		// 如果我们花费的时间超过10毫秒（例如，如果操作系统安排我们离开，或者某人
		// 让他们的机器进入睡眠状态）在关键部分，将我们使用的时间限制为
		// 以10毫秒计算，以避免让睡眠时间变得任意高。
		const maxCrit = 10e6
		if crit > maxCrit {
			crit = maxCrit
		}

		// 计算睡眠时间量，假设我们希望最多使用
		// 清除CPU时间的百分比。考虑到日程安排的开销，这可能会延长我们的睡眠时间，乘以我们离理想比率的距离。例如，如果我们睡得太多，那么我们将调整睡眠时间。
		adjust := scavengeEWMA / idealFraction
		sleepTime := int64(adjust * crit / (scavengePercent / 100.0))

		// 去睡觉吧。
		slept := scavengeSleep(sleepTime)

		// 计算新比率。
		fraction := crit / (crit + float64(slept))

		// 设置分数的下限。
		// 由于操作系统相关的异常，我们可能会“睡眠”过多的时间。让我们通过限制我们在EWMA中使用的睡眠时间来避免比率失控。
		const minFraction = 1.0 / 1000.0
		if fraction < minFraction {
			fraction = minFraction
		}

		// 通过合并新的crit/sleep比率来更新cleaveneewma。
		const alpha = 0.5
		scavengeEWMA = alpha*fraction + (1-alpha)*scavengeEWMA
	}
}

// 清除清除N字节的免费页面，首先从
// 最高地址开始。连续的调用从它关闭
// 的位置继续，直到堆耗尽。调用cleavenestartgen将其带回到堆的顶部。
// 
// 返回以字节为单位清除的内存量。
// 
// p.mheapLock必须保持，但如果
// mayulock==true，则可以临时释放。
// 
// 必须在系统堆栈上运行，因为必须持有p.mheapLock。
// 
// go:systemstack 
func (p *pageAlloc) scavenge(nbytes uintptr, mayUnlock bool) uintptr {
	assertLockHeld(p.mheapLock)

	var (
		addrs addrRange
		gen   uint32
	)
	released := uintptr(0)
	for released < nbytes {
		if addrs.size() == 0 {
			if addrs, gen = p.scavengeReserve(); addrs.size() == 0 {
				break
			}
		}
		r, a := p.scavengeOne(addrs, nbytes-released, mayUnlock)
		released += r
		addrs = a
	}
	// 只保留尚未清理或搜索的空间
	// 以确保我们始终取得进步。
	p.scavengeUnreserve(addrs, gen)
	return released
}

// printscapvtrace将扫掠跟踪线打印到标准错误。
// 
// released应该是自上次调用此
// 以来释放的内存量，强制表示是否由
// 应用程序强制执行清除。
func printScavTrace(gen uint32, released uintptr, forced bool) {
	printlock()
	print("scav ", gen, " ",
		released>>10, " KiB work, ",
		atomic.Load64(&memstats.heap_released)>>10, " KiB total, ",
		(atomic.Load64(&memstats.heap_inuse)*100)/heapRetained(), "% util",
	)
	if forced {
		print(" (forced)")
	}
	println()
	printunlock()
}

// 清道夫启动新一代清道夫，将清道夫的搜索空间重置为完全使用的地址空间。
// 
// p.mheapLock必须保持。
// 
// 必须在系统堆栈上运行，因为必须持有p.mheapLock。
// 
// go:systemstack 
func (p *pageAlloc) scavengeStartGen() {
	assertLockHeld(p.mheapLock)

	if debug.scavtrace > 0 {
		printScavTrace(p.scav.gen, p.scav.released, false)
	}
	p.inUse.cloneInto(&p.scav.inUse)

	// 为清洗器循环选择新的起始地址。
	var startAddr offAddr
	if p.scav.scavLWM.lessThan(p.scav.freeHWM) {
		// 免费的高水位线超过了“清除的”低水位线，
		// 因此在地址空间的部分
		// 中有免费的可清除页面，清除程序已经搜索过，高水位线是最高水位线。选择它作为我们的新起点，以确保我们看到这些页面。
		startAddr = p.scav.freeHWM
	} else {
		// 自由高水位线不超过清除低水位线
		// 水位线。这意味着分配器没有释放
		// 上一个周期清除的范围内的任何内存，因此我们不妨继续从我们所在的位置清除
		// 内存。
		startAddr = p.scav.scavLWM
	}
	p.scav.inUse.removeGreaterEqual(startAddr.addr())

	// 如果p.inUse.totalBytes较小，或者如果
	// 清除保留碎片较大，则reservationBytes可能为零。这种情况很好，因为清道夫
	// 将被简单地关闭，但这确实意味着清道夫保留碎片
	// 与pallocChunkBytes一起指示清道夫触发的最小堆大小。实际上，这个最小值通常小于
	// 竞技场的大小，因此实际上每个堆上都有清道夫。
	p.scav.reservationBytes = alignUp(p.inUse.totalBytes, pallocChunkBytes) / scavengeReservationShards
	p.scav.gen++
	p.scav.released = 0
	p.scav.freeHWM = minOffAddr
	p.scav.scavLWM = maxOffAddr
}

// 清道夫服务器保留一个连续的地址空间范围
// 用于清道夫。它保留的最大空间量与堆的大小成正比。从高位地址
// 开始保留范围。
// 
// 返回保留范围及其清除生成号。
// 
// p.mheapLock必须保持。必须在系统堆栈上运行，因为必须持有p.mheapLock。
// 
// go:systemstack 
func (p *pageAlloc) scavengeReserve() (addrRange, uint32) {
	assertLockHeld(p.mheapLock)

	// 从保留最小值开始。
	r := p.scav.inUse.removeLast(p.scav.reservationBytes)

	// 如果大小为零，请提前返回；我们不想使用
	// 下面的假地址。
	if r.size() == 0 {
		return r, p.scav.gen
	}

	// 清道夫要求碱基与
	// palloc块对齐，因为这是
	// 清道夫的操作单元，所以向下对齐，可能会扩展
	// 范围。
	newBase := alignDown(r.base.addr(), pallocChunkBytes)

	// 无论我们刚刚拿出多少额外的钱，都要从使用中删除。
	p.scav.inUse.removeGreaterEqual(newBase)
	r.base = offAddr{newBase}
	return r, p.scav.gen
}

// 清道夫恢复返回先前由清道夫服务保留的范围的未复仇部分。
// 
// p.mheapLock必须保持。
// 
// 必须在系统堆栈上运行，因为必须持有p.mheapLock。
// 
// go:systemstack 
func (p *pageAlloc) scavengeUnreserve(r addrRange, gen uint32) {
	assertLockHeld(p.mheapLock)

	if r.size() == 0 || gen != p.scav.gen {
		return
	}
	if r.base.addr()%pallocChunkBytes != 0 {
		throw("unreserving unaligned region")
	}
	p.scav.inUse.add(r)
}

// CleaveOne遍历地址范围工作，直到找到
// 要清除的连续页面运行。它将尝试一次最多清除最大字节数
// 但可能会清除更多字节以避免
// 破坏巨大的页面。一旦它清除了一些内存，它返回
// 它清除了多少字节。返回已清除的字节数和尚未搜索的工作部分。
// 
// 作品的基址必须与pallocChunkBytes对齐。
// 
// p.mheapLock必须保持，但如果
// mayulock==true，则可能会临时释放。
// 
// 必须在系统堆栈上运行，因为必须持有p.mheapLock。
// 
// go:systemstack 
func (p *pageAlloc) scavengeOne(work addrRange, max uintptr, mayUnlock bool) (uintptr, addrRange) {
	assertLockHeld(p.mheapLock)

	// 防御检查我们是否收到了空地址范围。
	// 如果是，请返回。
	if work.size() == 0 {
		// 无事可做。
		return 0, work
	}
	// 检查工作的先决条件。
	if work.base.addr()%pallocChunkBytes != 0 {
		throw("scavengeOne called with unaligned work region")
	}
	// 计算要清除的最大页数。
	// 
	// 这应该是对齐的（max，pageSize）/pageSize，但是max可以并且将会是^uintptpr（0），所以我们需要非常小心，不要在这里溢出。
	// 首先计算向下舍入的页数，而不是使用alignUp，如果需要，再加上一页。
	maxPages := max / pageSize
	if max%pageSize != 0 {
		maxPages++
	}

	// 计算我们可以清除的最小页数。
	// 
	// 因为我们只能清除整个物理页面，所以我们必须
	// 确保每次至少清除minPages，将
	// 与minPages*pageSize对齐。
	minPages := physPageSize / pageSize
	if minPages < 1 {
		minPages = 1
	}

	// 仅当mayUnlock==true时用于锁定和解锁的帮助程序。
	lockHeap := func() {
		if mayUnlock {
			lock(p.mheapLock)
		}
	}
	unlockHeap := func() {
		if mayUnlock {
			unlock(p.mheapLock)
		}
	}

	// 快速路径：检查包含工作中最顶层地址的区块，
	// 从区块中该地址的页面索引开始。
	// 
	// 注意work.end（）是独占的，所以通过减去1得到我们关心的块。
	maxAddr := work.limit.addr() - 1
	maxChunk := chunkIndex(maxAddr)
	if p.summary[len(p.summary)-1][maxChunk].max() >= uint(minPages) {
		// 只有在至少有免费页面的情况下，我们才会费心寻找候选人。
		base, npages := p.chunkOf(maxChunk).findScavengeCandidate(chunkPageIndex(maxAddr), minPages, maxPages)

		// 如果我们发现了什么东西，就把它清理干净并归还！
		if npages != 0 {
			work.limit = offAddr{p.scavengeRangeLocked(maxChunk, base, npages)}

			assertLockHeld(p.mheapLock) // 返回时必须锁定。
			return uintptr(npages) * pageSize, work
		}
	}
	// 更新限制以反映我们已检查maxChunk的事实。
	work.limit = offAddr{chunkBase(maxChunk)}

	// findCandidate乐观地找到工作中的下一个清道夫候选人。
	// 
	// 返回候选块索引，成功时返回true，失败时返回false。
	// 
	// 堆不需要锁定。
	findCandidate := func(work addrRange) (chunkIdx, bool) {
		// 迭代此工作的块。
		for i := chunkIndex(work.limit.addr() - 1); i >= chunkIndex(work.base.addr()); i-- {
			// 如果此区块完全在使用中或没有未屏蔽的页面，请不要麻烦
			// 进行更复杂的检查。
			// 
			// 注意，我们在访问摘要和块时没有锁定，但是
			// 没关系。无论如何，我们都很乐观。

			// 快速检查是否有足够的免费页面。
			if p.summary[len(p.summary)-1][i].max() < uint(minPages) {
				continue
			}

			// 仔细检查这块区域，寻找更难的候选人。同样，我们可以与许多不同的代码进行竞争，但我们只是乐观而已。但是，请确保以原子方式加载l2指针，以避免堆增长引起的争用。在这种情况下，如果我们与堆增长竞争，可能也可能不可能看到一个nil指针，但是只需防御性地忽略nil即可。无论如何，这一行动是乐观的。
			l2 := (*[1 << pallocChunksL2Bits]pallocData)(atomic.Loadp(unsafe.Pointer(&p.chunks[i.l1()])))
			if l2 != nil && l2[i.l2()].hasScavengeCandidate(minPages) {
				return i, true
			}
		}
		return 0, false
	}

	// 慢路径：在正在使用的地址空间中乐观地迭代
	// 查找任何免费且未屏蔽的页面。如果我们认为我们看到了什么，请锁定并验证它！
	for work.size() != 0 {
		unlockHeap()

		// 搜索候选人。
		candidateChunkIdx, ok := findCandidate(work)

		// 锁定堆。如果我们找到了候选人或没有，我们现在就需要这样做。
		// 如果我们这样做了，我们会核实的。如果没有，我们需要在返回
		// /之前锁定。
		lockHeap()

		if !ok {
			// 我们没有找到候选人，所以我们结束了。
			work.limit = work.base
			break
		}

		// 如果可以，请查找、验证和清除。
		chunk := p.chunkOf(candidateChunkIdx)
		base, npages := chunk.findScavengeCandidate(pallocChunkPages-1, minPages, maxPages)
		if npages > 0 {
			work.limit = offAddr{p.scavengeRangeLocked(candidateChunkIdx, base, npages)}

			assertLockHeld(p.mheapLock) // 返回时必须锁定。
			return uintptr(npages) * pageSize, work
		}

		// 我们被愚弄了，所以让我们从我们结束的地方继续。
		work.limit = offAddr{chunkBase(candidateChunkIdx)}
	}

	assertLockHeld(p.mheapLock) // 返回时必须锁定。
	return 0, work
}

// 清道夫锁定清除给定内存区域。
// 内存区域由其块索引（ci）描述，
// 该区域相对于该块的起始页索引
// 块（基），以及区域的长度（以页为单位）。返回被清除区域的基址。
// 
// p.mheapLock必须保持。
func (p *pageAlloc) scavengeRangeLocked(ci chunkIdx, base, npages uint) uintptr {
	assertLockHeld(p.mheapLock)

	p.chunkOf(ci).scavenged.setRange(base, npages)

	// 计算范围开始的完整地址。
	addr := chunkBase(ci) + uintptr(base)*pageSize

	// 更新清除低水位线。
	if oAddr := (offAddr{addr}); oAddr.lessThan(p.scav.scavLWM) {
		p.scav.scavLWM = oAddr
	}

	// 只有在不进行测试的情况下才执行实际的清除。
	// 否则这样做是危险的。
	if p.test {
		return addr
	}
	sysUnused(unsafe.Pointer(addr), uintptr(npages)*pageSize)

	// 仅当不在测试中时更新全局记帐，否则
	// 运行时的记帐将出错。
	nbytes := int64(npages) * pageSize
	atomic.Xadd64(&memstats.heap_released, nbytes)

	// 也更新一致的记帐。
	stats := memstats.heapStats.acquire()
	atomic.Xaddint64(&stats.committed, -nbytes)
	atomic.Xaddint64(&stats.released, nbytes)
	memstats.heapStats.release()

	return addr
}

// fillAligned返回x，但m对齐
// 如果组中的任何位不为零，则m位组中的所有零都设置为1。例如，fillAligned（0x0100a3，8）=0xff00ff。注意，如果m==1，这是不可操作的。
// 
// m必须是2的幂<=maxPagesPerPhysPage。
func fillAligned(x uint64, m uint) uint64 {
	apply := func(x uint64, c uint64) uint64 {
		// 此处使用的技术源自
		// https:
		// 并通过使用适当的常量扩展到不仅仅是字节（如半字节
		// 和uint16s）。
		// 
		// 为了总结这项技术，引用该页的内容：“
		// /”[It]首先将[8]的高位归零
		// 字中的字节。随后，它添加一个数字，如果初始设置了
		// 将导致字节高位溢出。接下来，原始字的高位
		// 任何低位，则
		// 位与这些值进行或运算；
		// 因此，高位为如果设置了
		// 字节中的任何位，则设置一个字节的值。最后，我们通过对除
		// 位中是否有位为零。”
		// 高位之外的所有位进行OR运算并反转结果，确定这些高位
		return ^((((x & c) + c) | x) | c)
	}
	// 将x变换为在每个m对齐的
	// 一组m零位的顶部包含1位。
	switch m {
	case 1:
		return x
	case 2:
		x = apply(x, 0x5555555555555555)
	case 4:
		x = apply(x, 0x7777777777777777)
	case 8:
		x = apply(x, 0x7f7f7f7f7f7f7f7f)
	case 16:
		x = apply(x, 0x7fff7fff7fff7fff)
	case 32:
		x = apply(x, 0x7fffffff7fffffff)
	case 64: // ==maxPagesPerPhysPage 
		x = apply(x, 0x7fffffffffffffff)
	default:
		throw("bad m value")
	}
	// 现在，设置x中每个m对齐组的顶部位
	// 该组在原始x中均为零。

	// 从每组m位中减去1。
	// 因为我们只知道设置了每个
	// m对齐组的顶部位，所以我们知道这将
	// 设置每个组的所有位，除了
	// 顶部位，所以只需或使用原始
	// 即可设置所有位。
	return ^((x - (x >> (m - 1))) | x)
}

// hasScavengeCandidate如果在此
// min页空闲和未屏蔽内存组，则返回true。
// pallocData表示的区域中存在任何最小页对齐的
// 
// min必须是2的非零幂<=maxPagesPerPhysPage。
func (m *pallocData) hasScavengeCandidate(min uintptr) bool {
	if min&(min-1) != 0 || min == 0 {
		print("runtime: min = ", min, "\n")
		throw("min must be a non-zero power of 2")
	} else if min > maxPagesPerPhysPage {
		print("runtime: min = ", min, "\n")
		throw("min too large")
	}

	// 此搜索的目标是查看该块是否包含任何可用的未屏蔽内存。ABCFDG 
	for i := len(m.scavenged) - 1; i >= 0; i-- {
		// 但会增加一些额外的代码复杂性。
		x := fillAligned(m.scavenged[i]|m.pallocBits[i], uint(min))

		// 快速跳过非免费或已清除的页面块。
		if x != ^uint64(0) {
			return true
		}
	}
	return false
}

// findScavengeCandidate返回此pallocData 
// 段的开始索引和大小，该段表示可用和未屏蔽内存的连续区域。
// 
// searchIdx指示此区块内的页面索引以开始搜索，但
// 请注意，findScavengeCandidate通过pallocData向后搜索。作为
// 结果，它将按地址顺序返回最高的清除候选地址。
// 
// min表示页面运行的最小硬大小和对齐方式。也就是说，
// findScavengeCandidate将不会返回小于最小页面大小的区域，
// 或最小页面大小或更大但未与最小页面对齐的区域。min必须是
// 非零次幂2<=maxPagesPerPhysPage。
// 
// max是一个提示，表示需要多大的区域。如果max>=pallocChunkPages，则
// findScavengeCandidate有效返回整个空闲和未屏蔽区域。
// 如果max<pallochunkpages，它可能会截断返回的区域，使其大小为
// max。但是，如果对于
// 示例，它选择保留巨大的页面，或者如果max未与min对齐（它将取整），findScavengeCandidate仍然可能返回更大的区域。也就是说，即使max很小，返回的大小也不能保证
// 等于max。max允许小于min，在这种情况下，就好像
// max==min。
func (m *pallocData) findScavengeCandidate(searchIdx uint, min, max uintptr) (uint, uint) {
	if min&(min-1) != 0 || min == 0 {
		print("runtime: min = ", min, "\n")
		throw("min must be a non-zero power of 2")
	} else if min > maxPagesPerPhysPage {
		print("runtime: min = ", min, "\n")
		throw("min too large")
	}
	// max可能没有最小对齐一样，因此，我们可能会意外地截断为
	// 一个最大值，这会导致我们返回一个非最小对齐值。
	// 为防止出现这种情况，请将最大值与最小值的倍数对齐（始终为
	// 2的幂）。这还可以防止max永远小于
	// min，除非它为零，所以请显式处理它。
	if max == 0 {
		max = min
	} else {
		max = alignUp(max, min)
	}

	i := int(searchIdx / 64)
	// 从快速跳过非空闲或已清除页面块开始。
	for ; i >= 0; i-- {
		// 1已清除或非自由=>0未清除和自由
		x := fillAligned(m.scavenged[i]|m.pallocBits[i], uint(min))
		if x != ^uint64(0) {
			break
		}
	}
	if i < 0 {
		// 找不到任何自由/未清除的页面。
		return 0, 0
	}
	// 我们在i的64位块中有一些东西，但它可以进一步扩展。循环直到我们找到它的范围。

	// 1s被清除或非自由=>0s未被清除和自由
	x := fillAligned(m.scavenged[i]|m.pallocBits[i], uint(min))
	z1 := uint(sys.LeadingZeros64(^x))
	run, end := uint(0), uint(i)*64+(64-z1)
	if x<<z1 != 0 {
		// 在移出z1位后，我们仍然有1s，
		// 因此运行在这个字内结束。
		run = uint(sys.LeadingZeros64(x << z1))
	} else {
		// 移出z1位后，我们没有更多的1。
		// 这意味着运行延伸到
		// 单词的底部，因此它可能会延伸到更多单词。
		run = 64 - z1
		for j := i - 1; j >= 0; j-- {
			x := fillAligned(m.scavenged[j]|m.pallocBits[j], uint(min))
			run += uint(sys.LeadingZeros64(x))
			if x != 0 {
				// 在这个单词中运行停止。
				break
			}
		}
	}

	// 如果长度大于max，则拆分我们找到的跑步记录，但请保留原始长度，因为我们以后可能需要它。
	size := run
	if size > uint(max) {
		size = uint(max)
	}
	start := end - size

	// 保证每个巨大的页面都能放入一个palloc块中。
	// 
	// TODO（mknyszek）：支持更大的页面大小。ABCFDG
	if physHugePageSize > pageSize && physHugePageSize > physPageSize {
		// 我们有巨大的页面，所以让我们确保不会通过在巨大的页面边界上清除
		// 来破坏页面。如果范围[start，start+size]与
		// 一个免费且未经修补的巨大页面重叠，我们希望扩大我们清理的区域
		// 以包含该巨大页面。

		// 计算候选页面上方的巨大页面边界。
		pagesPerHugePage := uintptr(physHugePageSize / pageSize)
		hugePageAbove := uint(alignUp(uintptr(start), pagesPerHugePage))

		// 如果该边界在我们当前的候选页面内，那么我们可能会突破BCDEFG
		if hugePageAbove <= end {
			// 计算我们候选人下方的巨大页面边界。
			hugePageBelow := uint(alignDown(uintptr(start), pagesPerHugePage))

			if hugePageBelow >= end-run {
				// 由于start+size跨越了
				// 一个巨大的页面边界，并将其四舍五入到最近的巨大
				// 页面边界包含在完整的r中联合国我们发现。包括整个
				// 巨大的页面在装订中，向下舍入到巨大的页面大小。
				size = size + (start - hugePageBelow)
				start = hugePageBelow
			}
		}
	}
	return start, size
}
